• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2021 Google LLC
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16################################################################################
17"""Helper script for creating an llvm-cov style JSON summary from a JaCoCo XML
18report."""
19import json
20import os
21import sys
22import xml.etree.ElementTree as ET
23
24
25def convert(xml):
26  """Turns a JaCoCo XML report into an llvm-cov JSON summary."""
27  summary = {
28      "type": "oss-fuzz.java.coverage.json.export",
29      "version": "1.0.0",
30      "data": [{
31          "totals": {},
32          "files": [],
33      }],
34  }
35
36  report = ET.fromstring(xml)
37  totals = make_element_summary(report)
38  summary["data"][0]["totals"] = totals
39
40  # Since Java compilation does not track source file location, we match
41  # coverage info to source files via the full class name, e.g. we search for
42  # a path in /out/src ending in foo/bar/Baz.java for the class foo.bar.Baz.
43  # Under the assumptions that a given project only ever contains a single
44  # version of a class and that no class name appears as a suffix of another
45  # class name, we can assign coverage info to every source file matched in that
46  # way.
47  src_files = list_src_files()
48
49  for class_element in report.findall("./package/class"):
50    class_name = class_element.attrib["name"]
51    package_name = os.path.dirname(class_name)
52    if "sourcefilename" not in class_element.attrib:
53      continue
54    basename = class_element.attrib["sourcefilename"]
55    # This path is "foo/Bar.java" for the class element
56    # <class name="foo/Bar" sourcefilename="Bar.java">.
57    canonical_path = os.path.join(package_name, basename)
58
59    class_summary = make_element_summary(class_element)
60    summary["data"][0]["files"].append({
61        "filename": relative_to_src_path(src_files, canonical_path),
62        "summary": class_summary,
63    })
64
65  return json.dumps(summary)
66
67
68def list_src_files():
69  """Returns a map from basename to full path for all files in $OUT/$SRC."""
70  filename_to_paths = {}
71  out_path = os.environ["OUT"] + "/"
72  src_path = os.environ["SRC"]
73  src_in_out = out_path + src_path
74  for dirpath, _, filenames in os.walk(src_in_out):
75    for filename in filenames:
76      full_path = dirpath + "/" + filename
77      # Map /out//src/... to /src/...
78      src_path = full_path[len(out_path):]
79      filename_to_paths.setdefault(filename, []).append(src_path)
80  return filename_to_paths
81
82
83def relative_to_src_path(src_files, canonical_path):
84  """Returns all paths in src_files ending in canonical_path."""
85  basename = os.path.basename(canonical_path)
86  if basename not in src_files:
87    return []
88  candidate_paths = src_files[basename]
89  return [
90      path for path in candidate_paths if path.endswith("/" + canonical_path)
91  ]
92
93
94def make_element_summary(element):
95  """Returns a coverage summary for an element in the XML report."""
96  summary = {}
97
98  function_counter = element.find("./counter[@type='METHOD']")
99  summary["functions"] = make_counter_summary(function_counter)
100
101  line_counter = element.find("./counter[@type='LINE']")
102  summary["lines"] = make_counter_summary(line_counter)
103
104  # JaCoCo tracks branch coverage, which counts the covered control-flow edges
105  # between llvm-cov's regions instead of the covered regions themselves. For
106  # non-trivial code parts, the difference is usually negligible. However, if
107  # all methods of a class consist of a single region only (no branches),
108  # JaCoCo does not report any branch coverage even if there is instruction
109  # coverage. Since this would give incorrect results for CI Fuzz purposes, we
110  # increase the regions counter by 1 if there is any amount of instruction
111  # coverage.
112  instruction_counter = element.find("./counter[@type='INSTRUCTION']")
113  has_some_coverage = instruction_counter is not None and int(
114      instruction_counter.attrib["covered"]) > 0
115  branch_covered_adjustment = 1 if has_some_coverage else 0
116  region_counter = element.find("./counter[@type='BRANCH']")
117  summary["regions"] = make_counter_summary(
118      region_counter, covered_adjustment=branch_covered_adjustment)
119
120  return summary
121
122
123def make_counter_summary(counter_element, covered_adjustment=0):
124  """Turns a JaCoCo <counter> element into an llvm-cov totals entry."""
125  summary = {}
126  covered = covered_adjustment
127  missed = 0
128  if counter_element is not None:
129    covered += int(counter_element.attrib["covered"])
130    missed += int(counter_element.attrib["missed"])
131  summary["covered"] = covered
132  summary["notcovered"] = missed
133  summary["count"] = summary["covered"] + summary["notcovered"]
134  if summary["count"] != 0:
135    summary["percent"] = (100.0 * summary["covered"]) / summary["count"]
136  else:
137    summary["percent"] = 0
138  return summary
139
140
141def main():
142  """Produces an llvm-cov style JSON summary from a JaCoCo XML report."""
143  if len(sys.argv) != 3:
144    sys.stderr.write('Usage: %s <path_to_jacoco_xml> <out_path_json>\n' %
145                     sys.argv[0])
146    return 1
147
148  with open(sys.argv[1], 'r') as xml_file:
149    xml_report = xml_file.read()
150  json_summary = convert(xml_report)
151  with open(sys.argv[2], 'w') as json_file:
152    json_file.write(json_summary)
153
154  return 0
155
156
157if __name__ == "__main__":
158  sys.exit(main())
159