bot/code_coverage_bot/hooks/repo.py (152 lines of code) (raw):
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import os
import zipfile
from datetime import timedelta
import structlog
from code_coverage_bot import config
from code_coverage_bot import hgmo
from code_coverage_bot import uploader
from code_coverage_bot.cli import setup_cli
from code_coverage_bot.hooks.base import Hook
from code_coverage_bot.notifier import notify_email
from code_coverage_bot.phabricator import PhabricatorUploader
from code_coverage_bot.phabricator import parse_revision_id
from code_coverage_bot.secrets import secrets
from code_coverage_bot.taskcluster import taskcluster_config
from code_coverage_tools import gcp
logger = structlog.get_logger(__name__)
class RepositoryHook(Hook):
"""
Base class to support specific workflows per repository
"""
def upload_reports(self, reports):
"""
Upload all provided covdir reports on GCP
"""
for (platform, suite), path in reports.items():
report = open(path, "rb").read()
uploader.gcp(
self.branch, self.revision, report, suite=suite, platform=platform
)
def check_javascript_files(self):
"""
Check that all JavaScript files present in the coverage artifacts actually exist.
If they don't, there might be a bug in the LCOV rewriter.
"""
for artifact in self.artifactsHandler.get():
if "jsvm" not in artifact:
continue
with zipfile.ZipFile(artifact, "r") as zf:
for file_name in zf.namelist():
with zf.open(file_name, "r") as fl:
source_files = [
line[3:].decode("utf-8").rstrip()
for line in fl
if line.startswith(b"SF:")
]
missing_files = [
f
for f in source_files
if not os.path.exists(os.path.join(self.repo_dir, f))
]
if len(missing_files) != 0:
logger.warn(
f"{missing_files} are present in coverage reports, but missing from the repository"
)
def get_hgmo_changesets(self):
"""
Build HGMO changesets according to this repo's configuration
"""
with hgmo.HGMO(server_address=self.repository) as hgmo_server:
return hgmo_server.get_automation_relevance_changesets(self.revision)
def upload_phabricator(self, report, changesets):
"""
Helper to upload coverage report on Phabricator
"""
phabricatorUploader = PhabricatorUploader(self.repo_dir, self.revision)
logger.info("Upload changeset coverage data to Phabricator")
return phabricatorUploader.upload(report, changesets)
class MozillaCentralHook(RepositoryHook):
"""
Code coverage hook for mozilla-central
* Check coverage artifacts content
* Build all covdir reports possible
* Upload all reports on GCP
* Upload main reports on Phabrictaor
* Send an email to admins on low coverage
"""
def __init__(self, *args, **kwargs):
super().__init__(
config.MOZILLA_CENTRAL_REPOSITORY,
# On mozilla-central, we want to assert that every platform was run (except for android platforms
# as they are unstable).
required_platforms=["linux", "windows"],
*args,
**kwargs,
)
def run(self):
# Check the covdir report does not already exists
bucket = gcp.get_bucket(secrets[secrets.GOOGLE_CLOUD_STORAGE])
if uploader.gcp_covdir_exists(bucket, self.branch, self.revision, "all", "all"):
logger.warn("Full covdir report already on GCP")
return
# Generate and upload the full report as soon as possible, so it is available
# for consumers (e.g. Searchfox) right away.
self.retrieve_source_and_artifacts()
reports = self.build_reports(only=[("all", "all")])
full_path = reports.get(("all", "all"))
assert full_path is not None, "Missing full report (all:all)"
with open(full_path, "r") as f:
report_text = f.read()
# Upload report as an artifact.
taskcluster_config.upload_artifact(
"public/code-coverage-report.json",
report_text,
"application/json",
timedelta(days=14),
)
# Index on Taskcluster
self.index_task(
[
"project.relman.code-coverage.{}.repo.mozilla-central.{}".format(
secrets[secrets.APP_CHANNEL], self.revision
),
"project.relman.code-coverage.{}.repo.mozilla-central.latest".format(
secrets[secrets.APP_CHANNEL]
),
]
)
report = json.loads(report_text)
# Check extensions
paths = uploader.covdir_paths(report)
for extension in [".js", ".cpp"]:
assert any(
path.endswith(extension) for path in paths
), "No {} file in the generated report".format(extension)
# Upload coverage on phabricator
changesets = self.get_hgmo_changesets()
coverage = self.upload_phabricator(report, changesets)
# Send an email on low coverage
notify_email(self.revision, changesets, coverage)
logger.info("Sent low coverage email notification")
if secrets.get(secrets.CHECK_JAVASCRIPT_FILES, False):
self.check_javascript_files()
# Generate all reports except the full one which we generated earlier.
all_report_combinations = self.artifactsHandler.get_combinations()
del all_report_combinations[("all", "all")]
reports.update(self.build_reports())
logger.info("Built all covdir reports", nb=len(reports))
# Upload reports on GCP
self.upload_reports(reports)
logger.info("Uploaded all covdir reports", nb=len(reports))
class TryHook(RepositoryHook):
"""
Code coverage hook for a try push
* Build only main covdir report
* Upload that report on Phabrictaor
"""
def __init__(self, *args, **kwargs):
super().__init__(
config.TRY_REPOSITORY,
# On try, developers might have requested to run only one platform, and we trust them.
required_platforms=[],
*args,
**kwargs,
)
def run(self):
changesets = self.get_hgmo_changesets()
if not any(
parse_revision_id(changeset["desc"]) is not None for changeset in changesets
):
logger.info(
"None of the commits in the try push are linked to a Phabricator revision"
)
return
self.retrieve_source_and_artifacts()
reports = self.build_reports(only=[("all", "all")])
logger.info("Built all covdir reports", nb=len(reports))
# Retrieve the full report
full_path = reports.get(("all", "all"))
assert full_path is not None, "Missing full report (all:all)"
report = json.load(open(full_path))
# Upload coverage on phabricator
self.upload_phabricator(report, changesets)
# Index on Taskcluster
self.index_task(
[
"project.relman.code-coverage.{}.repo.try.{}".format(
secrets[secrets.APP_CHANNEL], self.revision
),
"project.relman.code-coverage.{}.repo.try.latest".format(
secrets[secrets.APP_CHANNEL]
),
]
)
def main():
logger.info("Starting code coverage bot for repository")
args = setup_cli()
hooks = {
config.MOZILLA_CENTRAL_REPOSITORY: MozillaCentralHook,
config.TRY_REPOSITORY: TryHook,
}
hook_class = hooks.get(args.repository)
assert hook_class is not None, f"Unsupported repository {args.repository}"
hook = hook_class(
args.revision, args.task_name_filter, args.cache_root, args.working_dir
)
hook.run()