|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | + |
| 3 | +# This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | +# License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | +# file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| 6 | + |
| 7 | +""" |
| 8 | +Produce coverage reports from the raw information produced by the |
| 9 | +`GLEAN_TEST_COVERAGE` feature. |
| 10 | +""" |
| 11 | + |
| 12 | +import json |
| 13 | +from .metrics import ObjectTree |
| 14 | +from pathlib import Path |
| 15 | +import sys |
| 16 | +from typing import Any, Dict, List, Optional, Sequence, Set |
| 17 | + |
| 18 | + |
| 19 | +from . import parser |
| 20 | +from . import util |
| 21 | + |
| 22 | + |
| 23 | +def _outputter_codecovio(metrics: ObjectTree, output_path: Path): |
| 24 | + """ |
| 25 | + Output coverage in codecov.io format as defined here: |
| 26 | +
|
| 27 | + https://docs.codecov.io/docs/codecov-custom-coverage-format |
| 28 | +
|
| 29 | + :param metrics: The tree of metrics, already annotated with coverage by |
| 30 | + `_annotate_coverage`. |
| 31 | + :param output_path: The file to output to. |
| 32 | + """ |
| 33 | + coverage: Dict[str, List] = {} |
| 34 | + for category in metrics.values(): |
| 35 | + for metric in category.values(): |
| 36 | + defined_in = metric.defined_in |
| 37 | + if defined_in is not None: |
| 38 | + path = defined_in["filepath"] |
| 39 | + if path not in coverage: |
| 40 | + with open(path) as fd: |
| 41 | + nlines = len(list(fd.readlines())) |
| 42 | + lines = [None] * nlines |
| 43 | + coverage[path] = lines |
| 44 | + file_section = coverage[path] |
| 45 | + file_section[int(defined_in["line"])] = getattr(metric, "covered", 0) |
| 46 | + |
| 47 | + with open(output_path, "w") as fd: |
| 48 | + json.dump({"coverage": coverage}, fd) |
| 49 | + |
| 50 | + |
| 51 | +OUTPUTTERS = {"codecovio": _outputter_codecovio} |
| 52 | + |
| 53 | + |
| 54 | +def _annotate_coverage(metrics, coverage_entries): |
| 55 | + """ |
| 56 | + Annotate each metric with whether it is covered. Sets the attribute |
| 57 | + `covered` to 1 on each metric that is covered. |
| 58 | + """ |
| 59 | + mapping = {} |
| 60 | + for category in metrics.values(): |
| 61 | + for metric in category.values(): |
| 62 | + mapping[metric.identifier()] = metric |
| 63 | + |
| 64 | + for entry in coverage_entries: |
| 65 | + metric_id = _coverage_entry_to_metric_id(entry) |
| 66 | + if metric_id in mapping: |
| 67 | + mapping[metric_id].covered = 1 |
| 68 | + |
| 69 | + |
| 70 | +def _coverage_entry_to_metric_id(entry: str) -> str: |
| 71 | + """ |
| 72 | + Convert a coverage entry to a metric id. |
| 73 | +
|
| 74 | + Technically, the coverage entries are rkv database keys, so are not just |
| 75 | + the metric identifier. This extracts the metric identifier part out. |
| 76 | + """ |
| 77 | + # If getting a glean error count, report it as covering the metric the |
| 78 | + # error occurred in, not the `glean.error.*` metric itself. |
| 79 | + if entry.startswith("glean.error."): |
| 80 | + entry = entry.split("/")[-1] |
| 81 | + # If a labeled metric, strip off the label part |
| 82 | + return entry.split("/")[0] |
| 83 | + |
| 84 | + |
| 85 | +def _read_coverage_entries(coverage_reports: List[Path]) -> Set[str]: |
| 86 | + """ |
| 87 | + Read coverage entries from one or more files, and deduplicates them. |
| 88 | + """ |
| 89 | + entries = set() |
| 90 | + |
| 91 | + for coverage_report in coverage_reports: |
| 92 | + with open(coverage_report) as fd: |
| 93 | + for line in fd.readlines(): |
| 94 | + entries.add(line.strip()) |
| 95 | + |
| 96 | + return entries |
| 97 | + |
| 98 | + |
| 99 | +def coverage( |
| 100 | + coverage_reports: List[Path], |
| 101 | + metrics_files: Sequence[Path], |
| 102 | + output_format: str, |
| 103 | + output_file: Path, |
| 104 | + parser_config: Optional[Dict[str, Any]] = None, |
| 105 | + file=sys.stderr, |
| 106 | +) -> int: |
| 107 | + """ |
| 108 | + Commandline helper for coverage. |
| 109 | +
|
| 110 | + :param coverage_reports: List of coverage report files, output from the |
| 111 | + Glean SDK when the `GLEAN_TEST_COVERAGE` environment variable is set. |
| 112 | + :param metrics_files: List of Path objects to load metrics from. |
| 113 | + :param output_format: The coverage output format to produce. Must be one of |
| 114 | + `OUTPUTTERS.keys()`. |
| 115 | + :param output_file: Path to output coverage report to. |
| 116 | + :param parser_config: Parser configuration object, passed to |
| 117 | + `parser.parse_objects`. |
| 118 | + :return: Non-zero if there were any errors. |
| 119 | + """ |
| 120 | + |
| 121 | + if parser_config is None: |
| 122 | + parser_config = {} |
| 123 | + |
| 124 | + if output_format not in OUTPUTTERS: |
| 125 | + raise ValueError(f"Unknown outputter {output_format}") |
| 126 | + |
| 127 | + metrics_files = util.ensure_list(metrics_files) |
| 128 | + |
| 129 | + all_objects = parser.parse_objects(metrics_files, parser_config) |
| 130 | + |
| 131 | + if util.report_validation_errors(all_objects): |
| 132 | + return 1 |
| 133 | + |
| 134 | + entries = _read_coverage_entries(coverage_reports) |
| 135 | + |
| 136 | + _annotate_coverage(all_objects.value, entries) |
| 137 | + |
| 138 | + OUTPUTTERS[output_format](all_objects.value, output_file) |
| 139 | + |
| 140 | + return 0 |
0 commit comments