diff --git a/.travis.yml b/.travis.yml index fcf4b082f1b..5373d111fb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,7 @@ before_script: - if [ "$TRAVIS_EVENT_TYPE" = "pull_request" ]; then contrib/devtools/commit-script-check.sh $TRAVIS_COMMIT_RANGE; fi - unset CC; unset CXX - if [ "$CHECK_DOC" = 1 ]; then contrib/devtools/check-doc.py; fi + - if [ "$CHECK_DOC" = 1 ]; then contrib/devtools/check-rpc-mappings.py .; fi - mkdir -p depends/SDKs depends/sdk-sources - if [ -n "$OSX_SDK" -a ! -f depends/sdk-sources/MacOSX${OSX_SDK}.sdk.tar.gz ]; then curl --location --fail $SDK_URL/MacOSX${OSX_SDK}.sdk.tar.gz -o depends/sdk-sources/MacOSX${OSX_SDK}.sdk.tar.gz; fi - if [ -n "$OSX_SDK" -a -f depends/sdk-sources/MacOSX${OSX_SDK}.sdk.tar.gz ]; then tar -C depends/SDKs -xf depends/sdk-sources/MacOSX${OSX_SDK}.sdk.tar.gz; fi diff --git a/contrib/devtools/check-rpc-mappings.py b/contrib/devtools/check-rpc-mappings.py new file mode 100755 index 00000000000..3e01bee6fd0 --- /dev/null +++ b/contrib/devtools/check-rpc-mappings.py @@ -0,0 +1,165 @@ +#!/usr/bin/python3 +''' +Check RPC argument consistency. +''' +import sys, os, re +from collections import defaultdict + +# Source files (relative to root) to scan for dispatch tables +SOURCES = [ +"src/rpc/server.cpp", +"src/rpc/blockchain.cpp", +"src/rpc/mining.cpp", +"src/rpc/misc.cpp", +"src/rpc/net.cpp", +"src/rpc/rawtransaction.cpp", +"src/wallet/rpcwallet.cpp", +] +# Source file (relative to root) containing conversion mapping +SOURCE_CLIENT = 'src/rpc/client.cpp' +# Argument names that should be ignored in consistency checks +IGNORE_DUMMY_ARGS = {'dummy','arg0','arg1','arg2','arg3','arg4','arg5','arg6','arg7','arg8','arg9'} + +def parse_string(s): + assert(s[0] == '"') + assert(s[-1] == '"') + return s[1:-1] + +def make_string(s): + return '"%s"' % s + +class RPCCommand: + def __init__(self, name, args): + self.name = name + self.args = args + +class RPCArgument: + def __init__(self, names, idx): + self.names = names + self.idx = idx + +def process_commands(fname): + ''' + Find and parse dispatch table in implementation file `fname`. + ''' + cmds = [] + in_rpcs = False + with open(fname, "r") as f: + for line in f: + line = line.rstrip() + if not in_rpcs: + if re.match("static const CRPCCommand .*\[\] =", line): + in_rpcs = True + else: + if line.startswith('};'): + in_rpcs = False + elif '{' in line and '"' in line: + m = re.search('{ *("[^"]*"), *("[^"]*"), *&([^,]*), *(true|false), *{([^}]*)} *},', line) + if not m: + print('No match: %s' % line) + exit(1) + name = parse_string(m.group(2)) + args_str = m.group(5).strip() + if args_str: + args = [RPCArgument(parse_string(x.strip()).split('|'), idx) for idx,x in enumerate(args_str.split(','))] + else: + args = [] + cmds.append(RPCCommand(name, args)) + assert(not in_rpcs) # If this assertion is hit, something went wrong with parsing the C++ file: update the regexps + return cmds + +def process_mapping(fname): + ''' + Find and parse conversion table in implementation file `fname`. + ''' + cmds = [] + in_rpcs = False + with open(fname, "r") as f: + for line in f: + line = line.rstrip() + if not in_rpcs: + if line == 'static const CRPCConvertParam vRPCConvertParams[] =': + in_rpcs = True + else: + if line.startswith('};'): + in_rpcs = False + elif '{' in line and '"' in line: + m = re.search('{ *("[^"]*"), *([0-9]+) *, *("[^"]*") *},', line) + if not m: + print('No match: %s' % line) + exit(1) + name = parse_string(m.group(1)) + idx = int(m.group(2)) + argname = parse_string(m.group(3)) + cmds.append((name, idx, argname)) + assert(not in_rpcs) + return cmds + + +def main(): + root = sys.argv[1] + + # Get all commands from dispatch tables + cmds = [] + for fname in SOURCES: + cmds += process_commands(os.path.join(root, fname)) + + cmds_by_name = {} + for cmd in cmds: + cmds_by_name[cmd.name] = cmd + + # Get current convert mapping for client + client = SOURCE_CLIENT + mapping_order = process_mapping(os.path.join(root, client)) + mapping = set(mapping_order) + + print('* Checking consistency between dispatch tables and vRPCConvertParams') + + # Check mapping consistency + errors = 0 + for (cmdname, argidx, argname) in mapping: + try: + rargnames = cmds_by_name[cmdname].args[argidx].names + except IndexError: + print('ERROR: %s argument %i (named %s in vRPCConvertParams) is not defined in dispatch table' % (cmdname, argidx, argname)) + errors += 1 + continue + if argname not in rargnames: + print('ERROR: %s argument %i is named %s in vRPCConvertParams but %s in dispatch table' % (cmdname, argidx, argname, rargnames), file=sys.stderr) + errors += 1 + + # Check for conflicts in vRPCConvertParams conversion + # All aliases for an argument must either be present in the + # conversion table, or not. Anything in between means an oversight + # and some aliases won't work. + for cmd in cmds: + for arg in cmd.args: + convert = [((cmd.name,arg.idx,argname) in mapping) for argname in arg.names] + if any(convert) != all(convert): + print('ERROR: %s argument %s has conflicts in vRPCConvertParams conversion specifier %s' % (cmd.name, arg.names, convert)) + errors += 1 + arg.convert = all(convert) + + # Check for conversion difference by argument name. + # It is preferable for API consistency that arguments with the same name + # have the same conversion, so bin by argument name. + all_methods_by_argname = defaultdict(list) + converts_by_argname = defaultdict(list) + for cmd in cmds: + for arg in cmd.args: + for argname in arg.names: + all_methods_by_argname[argname].append(cmd.name) + converts_by_argname[argname].append(arg.convert) + + for argname,convert in converts_by_argname.items(): + if all(convert) != any(convert): + if argname in IGNORE_DUMMY_ARGS: # these are testing or dummy, don't warn for them + continue + print('WARNING: conversion mismatch for argument named %s (%s)' % + (argname, list(zip(all_methods_by_argname[argname], converts_by_argname[argname])))) + + exit(errors > 0) + +if __name__ == '__main__': + main() +