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()