#!/usr/bin/env python3 import argparse import glob import os import re import subprocess import sys import tempfile import xml.etree.ElementTree USAGE = """ Compare go-ceph bindings coverage of the ceph api to available C code. Note that this script is not comprehensive and does not claim to be. It only takes functions into account and does not care about overlap in functionality (function x is a superset of y and thus y does not need to be implemented) or deprecated functions. Example: python3 apicompare.py --check=rbd --list . Example: python3 apicompare.py -crbd -crados -ccephfs . """ RBD_C = """ #include #include #define GNDN GNDN int foo(int x) { return x; } """ RADOS_C = """ #include #define GNDN GNDN int foo(int x) { return x; } """ CEPHFS_C = """ #define FILE_OFFSET_BITS 64 #include #define __USE_FILE_OFFSET64 #include #define GNDN GNDN int foo(int x) { return x; } """ class Results(object): def __init__(self): self.total = 0 self.referenced = [] self.documented = [] self.missing = [] def go_sources(srcdir): pat = '/'.join((srcdir, '*.go')) for fn in glob.glob(pat): if fn.endswith("_test.go"): continue yield fn def check_go(path, funcs): gotext = [] for fn in go_sources(path): with open(fn) as fh: gotext.append(fh.read()) gotext = '\n'.join(gotext) res = Results() res.total = len(funcs) for fname in funcs: regex = re.compile('\\bC\\.{}\\b'.format(fname)) m = regex.findall(gotext) if m: res.referenced.append(fname) else: res.missing.append(fname) return res def api_funcs(code_stub, prefix, dump_xml=False): funcs = [] with tempfile.TemporaryDirectory() as tempdir: tmpc = os.path.join(tempdir, 'temp.c') tmpxml = os.path.join(tempdir, 'temp_c.xml') with open(tmpc, 'w') as fh: fh.write(code_stub) subprocess.check_call( ['castxml', '--castxml-output=1', '-o', tmpxml, tmpc]) if dump_xml: with open(tmpxml) as fh: print(fh.read()) x = xml.etree.ElementTree.parse(tmpxml) for fe in x.getroot().findall('Function'): name = fe.attrib.get('name', '') if not name.startswith(prefix): continue funcs.append(name) return funcs def check_for_castxml(): try: with open(os.devnull) as nullfh: subprocess.check_call( ['castxml'], stdin=nullfh, stderr=nullfh, stdout=nullfh) except subprocess.CalledProcessError: sys.stderr.write('error: "castxml" binary must be installed\n') sys.exit(2) except FileNotFoundError: sys.stderr.write('error: "castxml" binary must be installed\n') sys.exit(2) def report(label, res, list_functions=False): print("{} functions covered: {}/{} : {:.2f}%".format( label, len(res.referenced), res.total, (100.0 * len(res.referenced)) / res.total)) print("{} functions remain: {}/{} : {:.2f}%".format( label, len(res.missing), res.total, (100.0 * len(res.missing)) / res.total)) if list_functions: for n in sorted(res.referenced): print(" Covered: {}".format(n)) for n in sorted(res.missing): print(" Missing: {}".format(n)) def main(): ap = argparse.ArgumentParser( description='compare api coverage extent', usage=USAGE) ap.add_argument( '--check', '-c', choices=['rbd', 'rados', 'cephfs'], action='append') ap.add_argument( '--list-functions', action='store_true') ap.add_argument('GO_SRC') cli = ap.parse_args() check_for_castxml() os.chdir(cli.GO_SRC) if 'rbd' in (cli.check or []): funcs = api_funcs(RBD_C, 'rbd_') res = check_go('rbd', funcs) report('RBD', res, cli.list_functions) if 'rados' in (cli.check or []): funcs = api_funcs(RADOS_C, 'rados_') res = check_go('rados', funcs) report('RADOS', res, cli.list_functions) if 'cephfs' in (cli.check or []): funcs = api_funcs(CEPHFS_C, 'ceph_') res = check_go('cephfs', funcs) report('CephFS', res, cli.list_functions) return if __name__ == '__main__': main()