#!/usr/bin/env python3 import argparse import os import requests import subprocess import sys import tempfile # Compare if a list is inside other list in order # From here: https://stackoverflow.com/a/20789669 def is_slice_in_list(s, l): len_s = len(s) return any(s == l[i:len_s + i] for i in range(len(l) - len_s + 1)) def get_src_path(package_name, colcon_ws): cmd = ['colcon', 'list', '--paths-only', '--base-paths', colcon_ws, '--packages-select', package_name] try: path = subprocess.check_output(cmd).decode('ascii').strip() except subprocess.CalledProcessError as e: print(e.output, file=sys.stderr) sys.exit(-1) if not path: print('Package not found: ' + input_pkg, file=sys.stderr) sys.exit(-1) assert len(path.splitlines()) == 1, 'Found more than one line returned by colcon list' return path if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('jenkins_coverage_build', help='URL of a ci.ro2s.org build using coverage (i.e https://ci.ros2.org/job/ci_linux_coverage/182)') parser.add_argument('ros_package', help='ROS package name to get the coverage rate from (i.e: rcutils)') args = parser.parse_args() input_url = args.jenkins_coverage_build input_pkg = args.ros_package r = requests.get(url=input_url + '/cobertura/api/json?depth=3') if r.status_code != 200: print('Wrong input URL ' + input_url, file=sys.stderr) sys.exit(-1) # Create colcon workspace, checkout sources with tempfile.TemporaryDirectory() as ros2_ws_path: ros2_repos_path = os.path.join(ros2_ws_path, 'ros2.repos') ros2_repos = requests.get('https://raw.githubusercontent.com/ros2/ros2/rolling/ros2.repos') if ros2_repos.status_code != requests.codes.ok: print('Failed to download ros2.repos file', file=sys.stderr) sys.exit(-1) with open(ros2_repos_path, 'wb') as file: file.write(ros2_repos.content) cmd = ['vcs', 'import', ros2_ws_path, '--shallow', '--retry', '5', '--input', ros2_repos_path] try: print('Getting ros2.repos sources to get packages source paths. Please wait') subprocess.check_output(cmd) except subprocess.CalledProcessError as e: print(e.output, file=sys.stderr) sys.exit(-1) # Get relative path in ROS workspace to real the package source code ros_package_path_in_ws = get_src_path(input_pkg, ros2_ws_path) input_pkg_rel_path = os.path.relpath(ros_package_path_in_ws, ros2_ws_path).split(os.path.sep) coverage_entries = r.json()['results']['children'] total_lines_under_testing = 0 total_lines_tested = 0 for e in coverage_entries: if e['name'] == '.': continue # e has children, elements or name entry_name = e['name'].replace("'", '') # search for the Lines coverage method lines_coverage = None for coverage_method in e['elements']: if coverage_method['name'] == 'Lines': lines_coverage = coverage_method assert lines_coverage, 'Did not find Lines metric in coverage' name_parts = entry_name.split('.') if len(name_parts) == 1: package_under_cov = name_parts[0] elif name_parts[0].startswith('test'): # no interest in test code continue elif name_parts[0].startswith('install'): # integration/system testing, out by now continue elif name_parts[0].startswith('build'): # in build the first part is always the ROS package name package_under_cov = name_parts[1] elif is_slice_in_list(input_pkg_rel_path, name_parts): # source code: check if coverage entry contains exactly the source path package_under_cov = input_pkg else: package_under_cov = 'NOT-FOUND' if package_under_cov == input_pkg: total_lines_under_testing += lines_coverage['denominator'] total_lines_tested += lines_coverage['numerator'] print(f' * {entry_name}' f" [{lines_coverage['ratio']:.2f}] --" f" {lines_coverage['numerator']:.0f}/{lines_coverage['denominator']:.0f}") if total_lines_under_testing == 0: print(f'No coverage found for package {input_pkg}', file=sys.stderr) sys.exit(-1) print(f'\nCombined unit testing for {input_pkg}:' f' {total_lines_tested / total_lines_under_testing * 100:.2f}%' f' {total_lines_tested:.0f}/{total_lines_under_testing:.0f}')