summaryrefslogtreecommitdiffstats
path: root/third_party/python/glean_parser/glean_parser/coverage.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--third_party/python/glean_parser/glean_parser/coverage.py140
1 files changed, 140 insertions, 0 deletions
diff --git a/third_party/python/glean_parser/glean_parser/coverage.py b/third_party/python/glean_parser/glean_parser/coverage.py
new file mode 100644
index 0000000000..776ea3183d
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/coverage.py
@@ -0,0 +1,140 @@
+# -*- 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/.
+
+"""
+Produce coverage reports from the raw information produced by the
+`GLEAN_TEST_COVERAGE` feature.
+"""
+
+import json
+from .metrics import ObjectTree
+from pathlib import Path
+import sys
+from typing import Any, Dict, List, Optional, Sequence, Set
+
+
+from . import parser
+from . import util
+
+
+def _outputter_codecovio(metrics: ObjectTree, output_path: Path):
+ """
+ Output coverage in codecov.io format as defined here:
+
+ https://docs.codecov.io/docs/codecov-custom-coverage-format
+
+ :param metrics: The tree of metrics, already annotated with coverage by
+ `_annotate_coverage`.
+ :param output_path: The file to output to.
+ """
+ coverage: Dict[str, List] = {}
+ for category in metrics.values():
+ for metric in category.values():
+ defined_in = metric.defined_in
+ if defined_in is not None:
+ path = defined_in["filepath"]
+ if path not in coverage:
+ with open(path) as fd:
+ nlines = len(list(fd.readlines()))
+ lines = [None] * nlines
+ coverage[path] = lines
+ file_section = coverage[path]
+ file_section[int(defined_in["line"])] = getattr(metric, "covered", 0)
+
+ with open(output_path, "w") as fd:
+ json.dump({"coverage": coverage}, fd)
+
+
+OUTPUTTERS = {"codecovio": _outputter_codecovio}
+
+
+def _annotate_coverage(metrics, coverage_entries):
+ """
+ Annotate each metric with whether it is covered. Sets the attribute
+ `covered` to 1 on each metric that is covered.
+ """
+ mapping = {}
+ for category in metrics.values():
+ for metric in category.values():
+ mapping[metric.identifier()] = metric
+
+ for entry in coverage_entries:
+ metric_id = _coverage_entry_to_metric_id(entry)
+ if metric_id in mapping:
+ mapping[metric_id].covered = 1
+
+
+def _coverage_entry_to_metric_id(entry: str) -> str:
+ """
+ Convert a coverage entry to a metric id.
+
+ Technically, the coverage entries are rkv database keys, so are not just
+ the metric identifier. This extracts the metric identifier part out.
+ """
+ # If getting a glean error count, report it as covering the metric the
+ # error occurred in, not the `glean.error.*` metric itself.
+ if entry.startswith("glean.error."):
+ entry = entry.split("/")[-1]
+ # If a labeled metric, strip off the label part
+ return entry.split("/")[0]
+
+
+def _read_coverage_entries(coverage_reports: List[Path]) -> Set[str]:
+ """
+ Read coverage entries from one or more files, and deduplicates them.
+ """
+ entries = set()
+
+ for coverage_report in coverage_reports:
+ with open(coverage_report) as fd:
+ for line in fd.readlines():
+ entries.add(line.strip())
+
+ return entries
+
+
+def coverage(
+ coverage_reports: List[Path],
+ metrics_files: Sequence[Path],
+ output_format: str,
+ output_file: Path,
+ parser_config: Optional[Dict[str, Any]] = None,
+ file=sys.stderr,
+) -> int:
+ """
+ Commandline helper for coverage.
+
+ :param coverage_reports: List of coverage report files, output from the
+ Glean SDK when the `GLEAN_TEST_COVERAGE` environment variable is set.
+ :param metrics_files: List of Path objects to load metrics from.
+ :param output_format: The coverage output format to produce. Must be one of
+ `OUTPUTTERS.keys()`.
+ :param output_file: Path to output coverage report to.
+ :param parser_config: Parser configuration object, passed to
+ `parser.parse_objects`.
+ :return: Non-zero if there were any errors.
+ """
+
+ if parser_config is None:
+ parser_config = {}
+
+ if output_format not in OUTPUTTERS:
+ raise ValueError(f"Unknown outputter {output_format}")
+
+ metrics_files = util.ensure_list(metrics_files)
+
+ all_objects = parser.parse_objects(metrics_files, parser_config)
+
+ if util.report_validation_errors(all_objects):
+ return 1
+
+ entries = _read_coverage_entries(coverage_reports)
+
+ _annotate_coverage(all_objects.value, entries)
+
+ OUTPUTTERS[output_format](all_objects.value, output_file)
+
+ return 0