diff --git a/contrib/apicompare.py b/contrib/apicompare.py new file mode 100755 index 0000000..38ef883 --- /dev/null +++ b/contrib/apicompare.py @@ -0,0 +1,181 @@ +#!/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()