blob: 92251fc807c7c1733130b2a952420249d6e8cbeb [file] [log] [blame]
"""Converts Jacoco XMl to LCOV.
- Resolves classes to sourcefiles
- Identifies classes without sourcefiles
- Identifies uninstrumented sourcefiles
- Reports packages and classes with most uncovered lines
"""
import itertools as IT
import os
import xml.etree.ElementTree as ET
def main():
# directory subtrees omitted from consideration
skip = [
# tests
'./tools/adt/idea/adt-ui-model/testSrc',
'./tools/adt/idea/adt-ui/src/test',
'./tools/adt/idea/android-adb/testSrc',
'./tools/adt/idea/android-common/test',
'./tools/adt/idea/android-debuggers/testSrc',
'./tools/adt/idea/android-kotlin/android-extensions-idea/testData',
'./tools/adt/idea/android-kotlin/android-extensions-idea/tests',
'./tools/adt/idea/android-kotlin/android-extensions-jps/testData',
'./tools/adt/idea/android-kotlin/idea-android/testData',
'./tools/adt/idea/android-kotlin/idea-android/tests',
'./tools/adt/idea/android-lang-databinding/testSrc',
'./tools/adt/idea/android-lang/testSrc',
'./tools/adt/idea/android-layout-inspector/testSrc',
'./tools/adt/idea/android-test-framework/testSrc',
'./tools/adt/idea/android-uitests/testData',
'./tools/adt/idea/android-uitests/testSrc',
'./tools/adt/idea/android/testData',
'./tools/adt/idea/android/testSrc',
'./tools/adt/idea/apkanalyzer/testSrc',
'./tools/adt/idea/assistant/testSrc',
'./tools/adt/idea/build-common/testSrc',
'./tools/adt/idea/connection-assistant/testSrc',
'./tools/adt/idea/databinding/testData',
'./tools/adt/idea/databinding/testSrc',
'./tools/adt/idea/deploy/testSrc',
'./tools/adt/idea/designer/testData',
'./tools/adt/idea/designer/testSrc',
'./tools/adt/idea/jps-plugin/testData',
'./tools/adt/idea/jps-plugin/testSrc',
'./tools/adt/idea/kotlin-integration/testData',
'./tools/adt/idea/kotlin-integration/testSrc',
'./tools/adt/idea/native-symbolizer/testSrc',
'./tools/adt/idea/observable-ui/testSrc',
'./tools/adt/idea/observable/testSrc',
'./tools/adt/idea/perfd-host/testSrc',
'./tools/adt/idea/profilers-android/testSrc',
'./tools/adt/idea/profilers-ui/testSrc',
'./tools/adt/idea/profilers/testSrc',
'./tools/adt/idea/project-system-gradle/testSrc',
'./tools/adt/idea/project-system/testSrc',
'./tools/adt/idea/sdk-updates/testSrc',
'./tools/adt/idea/smali/testSrc',
'./tools/adt/idea/swingp/testSrc',
'./tools/adt/idea/uitest-framework-bazel/testSrc',
'./tools/adt/idea/uitest-framework-gradle/testSrc',
'./tools/adt/idea/uitest-framework/testSrc',
'./tools/adt/idea/whats-new-assistant/testSrc',
'./tools/adt/idea/wizard-model/testSrc',
'./tools/analytics-library/crash/src/test',
'./tools/analytics-library/publisher/src/test',
'./tools/analytics-library/shared/src/test',
'./tools/analytics-library/testing/src/test',
'./tools/analytics-library/tracker/src/test',
'./tools/apksig/src/test',
'./tools/apkzlib/src/test',
'./tools/base/apkparser/analyzer/src/test',
'./tools/base/apkparser/binary-resources/src/test',
'./tools/base/apkparser/cli/src/test',
'./tools/base/bazel/test',
'./tools/base/bazel/testSrc',
'./tools/base/build-system/builder-model/src/test',
'./tools/base/build-system/builder-test-api/src/test',
'./tools/base/build-system/builder/src/test',
'./tools/base/build-system/gradle-api/src/test',
'./tools/base/build-system/gradle-core/src/test',
'./tools/base/build-system/gradle-experimental/src/test',
'./tools/base/build-system/instant-run-instrumentation/src/test',
'./tools/base/build-system/integration-test/application/src/test',
'./tools/base/build-system/integration-test/databinding/src/test',
'./tools/base/build-system/integration-test/framework/src/test',
'./tools/base/build-system/integration-test/java-library-model-builder/src/test',
'./tools/base/build-system/integration-test/test-projects',
'./tools/base/build-system/java-lib-plugin/java-lib-model-builder/src/test',
'./tools/base/build-system/manifest-merger/src/test',
'./tools/base/build-system/profile/src/test',
'./tools/base/common/src/test',
'./tools/base/ddmlib/src/test',
'./tools/base/deploy/deployer/src/test',
'./tools/base/deploy/test',
'./tools/base/devicelib/src/test',
'./tools/base/draw9patch/src/test',
'./tools/base/fakeadbserver/src/test',
'./tools/base/flags/src/test',
'./tools/base/instant-run/instant-run-client/src/test',
'./tools/base/instant-run/instant-run-runtime/src/test',
'./tools/base/layoutinspector/testSrc',
'./tools/base/layoutlib-api/sample/testproject',
'./tools/base/layoutlib-api/src/test',
'./tools/base/lint/libs/lint-gradle/src/test',
'./tools/base/lint/libs/lint-tests/src/test',
'./tools/base/ninepatch/src/test',
'./tools/base/perf-logger/src/test',
'./tools/base/perflib/src/test',
'./tools/base/pixelprobe/src/test',
'./tools/base/profiler/app/supportlib/src/test',
'./tools/base/profiler/integration-tests',
'./tools/base/profiler/tests',
'./tools/base/repository/src/test',
'./tools/base/sdk-common/src/test',
'./tools/base/sdklib/src/test',
'./tools/base/testutils/src/test',
'./tools/base/tracer/agent/testSrc',
'./tools/base/usb-devices/testSrc',
'./tools/base/vector-drawable-tool/src/test',
'./tools/buildSrc/src/test',
'./tools/data-binding/compilationTests/src/test',
'./tools/data-binding/compiler/src/test',
'./tools/data-binding/compilerCommon/src/test',
'./tools/data-binding/exec/src/test',
'./tools/data-binding/extensions-support/library/src/androidTest',
'./tools/data-binding/extensions/library/src/androidTest',
'./tools/data-binding/integration-tests-support',
'./tools/data-binding/integration-tests',
'./tools/data-binding/samples',
'./tools/dx/dalvik/dx/junit-tests',
'./tools/dx/dalvik/dx/tests',
# generated
'./tools/adt/idea/android-lang-databinding/gen',
'./tools/adt/idea/android-lang/gen',
'./tools/adt/idea/artwork/gen',
'./tools/adt/idea/smali/gen',
# external
'./tools/base/jobb',
'./tools/external',
'./tools/idea',
'./tools/sherpa',
'./tools/studio',
'./tools/swing-testing',
'./tools/swt',
'./tools/vendor',
]
# file extensions we care about
ends = [
'.java',
'.kt',
]
print 'create a filename -> dirpath map of ./tools'
# we're using a filename -> directory path because it makes resolving to
# sourcefiles very fast
filetree = {}
pathset = set()
# walk down the source file tree
for (dirpath, _, filenames) in os.walk('./tools'):
# ignore skipped directories
if any(IT.imap(dirpath.startswith, skip)):
continue
# otherwise add files to the map
for name in filenames:
if not any(IT.imap(name.endswith, ends)):
continue
if name in filetree:
filetree[name].add(dirpath)
else:
filetree[name] = set([dirpath])
pathset.add(os.path.join(dirpath, name))
print 'parse jacoco xml report'
root = ET.parse(
'./out/agent-coverage/tools/base/coverage_report/report.xml').getroot()
print 'resolve report package/file combos to directory paths'
# we're going to build a nest map like package -> (file -> path/coverage info)
# this makes it easy to do package or file level sorting for output
data = {}
matchedset = set()
fakeset = set()
for pkg in root.iter('package'):
pkg_name = pkg.get('name')
for sfile in pkg.iter('sourcefile'):
sfile_name = sfile.get('name')
if sfile_name in filetree:
# if there's a path with the package/class as a suffix, then that's the
# real sourcefile
matches = list(
IT.ifilter(lambda path: path.endswith(pkg_name),
filetree[sfile_name]))
if matches:
# the directory path upto the package portion
path = matches[0][0:matches[0].find(pkg_name)]
# the list of instrumented line numbers
instrumented = [int(line.get('nr')) for line in sfile.iter('line')]
# the set of covered line numbers
covered = set(
int(line.get('nr'))
for line in sfile.iter('line')
if line.get('ci') != '0')
if pkg_name not in data:
data[pkg_name] = {}
data[pkg_name][sfile_name] = {
'path': path,
'instrumented': instrumented,
'covered': covered,
}
matchedset.add(os.path.join(path, pkg_name, sfile_name))
else:
fakeset.add(os.path.join(pkg_name, sfile_name))
print 'compute unmatched real paths'
unmatched = pathset - matchedset
missing = open('./out/missing', 'w')
for path in sorted(unmatched):
missing.write('{}\n'.format(path))
print 'write fake package/class list'
fake = open('./out/fake', 'w')
for path in sorted(fakeset):
fake.write('{}\n'.format(path))
print 'write to lcov file'
lcov = open('./out/lcov', 'w')
for pkg_name in data:
for sfile_name in data[pkg_name]:
filepath = os.path.join(data[pkg_name][sfile_name]['path'], pkg_name,
sfile_name)
lcov.write('SF:{}\n'.format(filepath))
for line_num in data[pkg_name][sfile_name]['instrumented']:
# format is DA:{line number},{number of hits}, but we don't care about
# detailed hit numbers so just use 1 for covered lines
lcov.write('DA:{},{}\n'.format(
line_num, int(line_num in data[pkg_name][sfile_name]['covered'])))
lcov.write('end_of_record\n')
print 'aggregate package level coverage'
# we need to aggregate data to the package level for reports
pkg_cov = {}
for pkg in data:
inst = 0
cov = 0
paths = set()
for sfile in data[pkg]:
inst += len(data[pkg][sfile]['instrumented'])
cov += len(data[pkg][sfile]['covered'])
paths.add(data[pkg][sfile]['path'])
pkg_cov[pkg] = {
'instrumented': inst,
'covered': cov,
'paths': list(paths),
}
print 'write worst (most uncovered lines) report'
# packages + classes by uncovered lines
worst = open('./out/worst', 'w')
# packages only
worst_no_files = open('./out/worstNoFiles', 'w')
worst.write('uncovered lines - cov% : (package @ [paths])|(file @ path)\n')
worst.write('path to file = path/package/file rooted at WORKSPACE\n')
worst.write('NB: omits files and packages with zero uncovered lines\n')
worst_no_files.write('uncovered lines - cov% : package @ [paths]\n')
worst_no_files.write('path to package = path/package rooted at WORKSPACE\n')
worst_no_files.write('NB: omits packages with zero uncovered lines\n')
for pkg in sorted(
pkg_cov,
key=lambda p: pkg_cov[p]['instrumented'] - pkg_cov[p]['covered'],
reverse=True):
cov = pkg_cov[pkg]['covered']
inst = pkg_cov[pkg]['instrumented']
if inst - cov == 0: # no reason to list fully covered packages
continue
percent = round(100 * float(cov) / float(inst), 1)
worst.write('{} - {}% : {} @ {}\n'.format(inst - cov, percent, pkg,
pkg_cov[pkg]['paths']))
worst_no_files.write('{} - {}% : {} @ {}\n'.format(inst - cov, percent, pkg,
pkg_cov[pkg]['paths']))
def uncovered_line_count(file_cov):
return len(file_cov['instrumented']) - len(file_cov['covered'])
for sfile in sorted(
data[pkg],
key=lambda f: uncovered_line_count(data[pkg][f]),
reverse=True):
cov = len(data[pkg][sfile]['covered'])
inst = len(data[pkg][sfile]['instrumented'])
if inst - cov == 0:
continue
percent = round(100 * float(cov) / float(inst), 1)
worst.write(' {} - {}% : {} @ {}\n'.format(inst - cov, percent, sfile,
data[pkg][sfile]['path']))
if __name__ == '__main__':
main()