diff options
Diffstat (limited to 'test')
-rwxr-xr-x | test/tools/dump_coveralls.py | 66 | ||||
-rwxr-xr-x | test/tools/gcov_coveralls.py | 206 | ||||
-rwxr-xr-x | test/tools/merge_coveralls.py (renamed from test/functional/util/merge_coveralls.py) | 30 |
3 files changed, 284 insertions, 18 deletions
diff --git a/test/tools/dump_coveralls.py b/test/tools/dump_coveralls.py new file mode 100755 index 000000000..c453d0511 --- /dev/null +++ b/test/tools/dump_coveralls.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +# Small tool to dump JSON payload for coveralls.io API + +import json +from operator import itemgetter +import os +import sys + + +def warn(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def dump_file(json_file): + """Dumps coveralls.io API payload stored in json_file + Returns: 0 if successful, 1 otherwise + """ + try: + with open(json_file, encoding='utf8') as f: + data = json.load(f) + except OSError as err: + warn(err) + return os.EX_DATAERR + except json.decoder.JSONDecodeError: + warn("{}: json parsing error".format(json_file)) + return 1 + + if 'source_files' not in data: + warn("{}: no source_files, not a coveralls.io payload?".format(json_file)) + return 1 + + print("{} ({} soource files)".format(json_file, len(data['source_files']))) + + for src_file in sorted(data['source_files'], key=itemgetter('name')): + covered_lines = not_skipped_lines = 0 + for cnt in src_file['coverage']: + if cnt is None: + continue + not_skipped_lines += 1 + if cnt > 0: + covered_lines += 1 + if not_skipped_lines > 0: + coverage = "{:.0%}".format(covered_lines / not_skipped_lines) + else: + coverage = 'N/A' + + print("\t{:>3} {}".format(coverage, src_file['name'])) + + return 0 + + +def main(): + if (len(sys.argv) < 2): + warn("usage: {} file.json ...".format(sys.argv[0])) + return os.EX_USAGE + + exit_status = 0 + for f in sys.argv[1:]: + exit_status += dump_file(f) + + return exit_status + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/test/tools/gcov_coveralls.py b/test/tools/gcov_coveralls.py new file mode 100755 index 000000000..71aa48b7b --- /dev/null +++ b/test/tools/gcov_coveralls.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Script to save coverage info for C source files in JSON for coveralls.io + +When C code compiled with --coverage flag, for each object files *.gcno is +generated, it contains information to reconstruct the basic block graphs and +assign source line numbers to blocks + +When binary executed *.gcda file is written on exit, with same base name as +corresponding *.gcno file. It contains some summary information, counters, e.t.c. + +gcov(1) utility can be used to get information from *.gcda file and write text +reports to *.gocov file (one file for each source file from which object was compiled). + +The script finds *.gcno files, uses gcov to generate *.gcov files, parses them +and accomulates statistics for all source files. + +This script was written with quite a few assumptions: + + * Code was build using absolute path to source directory (and absolute path + stored in object file debug sylmbols). + + * Current directory is writable and there is no useful *.gcov files in it + (becase they will be deleted). + + * Object file has same base name as *.gcno file (e. g. foo.c.gcno and foo.c.o). + This is the case for cmake builds, but probably not for other build systems + + * Source file names contain only ASCII characters. +""" + +import argparse +from collections import defaultdict +from glob import glob +import hashlib +import json +import os +from os.path import isabs, join, normpath, relpath +import os.path +import subprocess +import sys + + +def warn(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def parse_gcov_file(gcov_file): + """Parses the content of .gcov file written by gcov --intermediate-format + + Returns: + str: Source file name + dict: coverage info { line_number: hits } + """ + count = {} + with open(gcov_file) as fh: + for line in fh: + tag, value = line.split(':') + if tag == 'file': + src_file = value.rstrip() + elif tag == 'lcount': + line_num, exec_count = value.split(',') + count[int(line_num)] = int(exec_count) + + return src_file, count + + +def run_gcov(filename, coverage, args): + """ * run gcov on given file + * parse generated .gcov files and update coverage structure + * store source file md5 (if not yet stored) + * delete .gcov files + """ + if args.verbose: + warn("calling:", 'gcov', '--intermediate-format', filename) + stdout = None + else: + # gcov is noisy and don't have quit flag so redirect stdout to /dev/null + stdout = subprocess.DEVNULL + + subprocess.check_call(['gcov', '--intermediate-format', filename], stdout=stdout) + + for gcov_file in glob('*.gcov'): + if args.verbose: + warn('parsing', gcov_file) + src_file, count = parse_gcov_file(gcov_file) + os.remove(gcov_file) + + if src_file not in coverage: + coverage[src_file] = defaultdict(int, count) + else: + # sum execution counts + for line, exe_cnt in count.items(): + coverage[src_file][line] += exe_cnt + + +def main(): + parser = argparse.ArgumentParser( + description='Save gcov coverage results in JSON file for coveralls.io.') + parser.add_argument( + '-v', + '--verbose', + action="store_true", + help='Display additional informaton and gcov command output.') + parser.add_argument( + '-e', + '--exclude', + action='append', + metavar='DIR', + help= + ("Don't look for .gcno/.gcda files in this directories (repeat option to skip several directories). " + "Path is relative to the dirictory where script was started, e. g. '.git'")) + parser.add_argument( + '-p', + '--prefix', + action='append', + help= + ("Strip this prefix from absolute path to source file. " + "If this option is provided, then only files with given prefixex in absolute path " + "will be added to coverage (option can be repeated).")) + parser.add_argument( + '--out', + type=argparse.FileType('w'), + required=True, + metavar='FILE', + help='Save JSON payload to this file') + args = parser.parse_args() + + # ensure that there is no unrelated .gcov files in current directory + for gcov_file in glob('*.gcov'): + os.remove(gcov_file) + warn("Warning: {} deleted".format(gcov_file)) + + # dict { src_file_name: {line1: exec_count1, line2: exec_count2, ...} } + coverage = {} + + # find . -name '*.gcno' (respecting args.exclude) + for root, dirs, files in os.walk('.'): + for f in files: + # Usually gcov called with a source file as an argument, but this + # name used only to find .gcno and .gcda files. To find source + # file information from debug symbols is used. So we can call gcov + # on .gcno file. + if f.endswith('.gcno'): + run_gcov(join(root, f), coverage, args) + + # don't look into excluded dirs + for subdir in dirs: + # path relative to start dir + path = normpath(join(root, subdir)) + if path in args.exclude: + if args.verbose: + warn('directory "{}" excluded'.format(path)) + dirs.remove(subdir) + + # prepare JSON pyload for coveralls.io API + # https://docs.coveralls.io/api-introduction + coveralls_data = {'source_files': []} + + for src_file in coverage: + # filter by prefix and save path with stripped prefix + src_file_rel = src_file + if args.prefix and isabs(src_file): + for prefix in args.prefix: + if src_file.startswith(prefix): + src_file_rel = relpath(src_file, start=prefix) + break + else: + # skip file outside given prefixes + # it can be e. g. library include file + if args.verbose: + warn('file "{}" is not mathced by prefix, skipping'.format(src_file)) + continue + + try: + with open(src_file, mode='rb') as fh: + line_count = sum(1 for _ in fh) + fh.seek(0) + md5 = hashlib.md5(fh.read()).hexdigest() + except OSError as err: + # skip files for which source file is not available + warn(err, 'not adding to coverage') + continue + + coverage_array = [None] * line_count + + for line_num, exe_cnt in coverage[src_file].items(): + # item at index 0 representing the coverage for line 1 of the source code + assert 1 <= line_num <= line_count + coverage_array[line_num - 1] = exe_cnt + + coveralls_data['source_files'].append({ + 'name': src_file_rel, + 'coverage': coverage_array, + 'source_digest': md5 + }) + + args.out.write(json.dumps(coveralls_data)) + + if args.verbose: + warn('Coverage for {} source files was written'.format( + len(coveralls_data['source_files']))) + + +if __name__ == '__main__': + main() diff --git a/test/functional/util/merge_coveralls.py b/test/tools/merge_coveralls.py index c3976b48f..8fad0f55b 100755 --- a/test/functional/util/merge_coveralls.py +++ b/test/tools/merge_coveralls.py @@ -37,12 +37,13 @@ path_mapping = [ ] parser = argparse.ArgumentParser(description='') -parser.add_argument('--input', type=str, required=True, nargs='+', help='input files') -parser.add_argument('--output', type=str, required=True, help='output file)') -parser.add_argument('--root', type=str, required=False, default="/rspamd/src/github.com/rspamd/rspamd", help='repository root)') -parser.add_argument('--install-dir', type=str, required=False, default="/rspamd/install", help='install root)') -parser.add_argument('--build-dir', type=str, required=False, default="/rspamd/build", help='build root)') -parser.add_argument('--token', type=str, help='If present, the file will be uploaded to coveralls)') +parser.add_argument('--input', required=True, nargs='+', help='input files') +parser.add_argument('--output', help='output file)') +parser.add_argument('--root', default="/rspamd/src/github.com/rspamd/rspamd", help='repository root)') +parser.add_argument('--install-dir', default="/rspamd/install", help='install root)') +parser.add_argument('--build-dir', default="/rspamd/build", help='build root)') +parser.add_argument('--token', help='If present, the file will be uploaded to coveralls)') + def merge_coverage_vectors(c1, c2): assert(len(c1) == len(c2)) @@ -85,11 +86,6 @@ def merge(files, j1): else: sf['name'] = name files[name] = sf - if not ('source' in sf): - path = "%s/%s" % (repository_root, sf['name']) - if os.path.isfile(path): - with open(path) as f: - files[name]['source'] = f.read() return files @@ -127,11 +123,10 @@ if __name__ == '__main__': if 'service_job_id' not in j1 and 'service_job_id' in j2: j1['service_job_id'] = j2['service_job_id'] - if not j1['service_job_id'] and 'CIRCLE_BUILD_NUM' in os.environ: - j1['service_job_id'] = os.environ['CIRCLE_BUILD_NUM'] - if 'CIRCLECI' in os.environ and os.environ['CIRCLECI']: + if os.getenv('CIRCLECI'): j1['service_name'] = 'circleci' + j1['service_job_id'] = os.getenv('CIRCLE_BUILD_NUM') elif os.getenv('CI') == 'drone': j1['service_name'] = 'drone' j1['service_branch'] = os.getenv('CI_COMMIT_BRANCH') @@ -159,8 +154,9 @@ if __name__ == '__main__': j1['source_files'] = list(files.values()) - with open(args.output, 'w') as f: - f.write(json.dumps(j1)) + if args.output: + with open(args.output, 'w') as f: + f.write(json.dumps(j1)) if args.token: j1['repo_token'] = args.token @@ -173,5 +169,3 @@ if __name__ == '__main__': # post https://coveralls.io/api/v1/jobs # print args - - |