Skip to content

Commit 77aa9e5

Browse files
committed
test: Check RPC argument mapping
Parse the dispatch tables from the server implementation files, and the conversion table from the client. Perform the following consistency checks: - Arguments defined in conversion table, must be present in dispatch table. If not, it was probably forgotten to add them to the dispatch table, and they will not work. - Arguments defined in conversion table must have the same names as in the dispatch table. If not, they will not work. - 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. Any of these results in an error. It also performs a consistency check to see if the same named argument is sometimes converted, and sometimes not. E.g. one RPC call might have a 'verbose' argument that is converted, another RPC call might have one that is not converted. This is not necessarily wrong, but points at a possible error (as well as makes the API harder to memorize) - so it is emitted as a warning (could upgrade this to error).
1 parent 52f8877 commit 77aa9e5

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ install:
4545
before_script:
4646
- if [ "$CHECK_DOC" = 1 -a "$TRAVIS_EVENT_TYPE" = "pull_request" ]; then contrib/devtools/commit-script-check.sh $TRAVIS_COMMIT_RANGE; fi
4747
- if [ "$CHECK_DOC" = 1 ]; then contrib/devtools/check-doc.py; fi
48+
- if [ "$CHECK_DOC" = 1 ]; then contrib/devtools/check-rpc-mappings.py .; fi
4849
- unset CC; unset CXX
4950
- mkdir -p depends/SDKs depends/sdk-sources
5051
- 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
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2017 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Check RPC argument consistency."""
6+
7+
from collections import defaultdict
8+
import os
9+
import re
10+
import sys
11+
12+
# Source files (relative to root) to scan for dispatch tables
13+
SOURCES = [
14+
"src/rpc/server.cpp",
15+
"src/rpc/blockchain.cpp",
16+
"src/rpc/mining.cpp",
17+
"src/rpc/misc.cpp",
18+
"src/rpc/net.cpp",
19+
"src/rpc/rawtransaction.cpp",
20+
"src/wallet/rpcwallet.cpp",
21+
]
22+
# Source file (relative to root) containing conversion mapping
23+
SOURCE_CLIENT = 'src/rpc/client.cpp'
24+
# Argument names that should be ignored in consistency checks
25+
IGNORE_DUMMY_ARGS = {'dummy', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6', 'arg7', 'arg8', 'arg9'}
26+
27+
class RPCCommand:
28+
def __init__(self, name, args):
29+
self.name = name
30+
self.args = args
31+
32+
class RPCArgument:
33+
def __init__(self, names, idx):
34+
self.names = names
35+
self.idx = idx
36+
self.convert = False
37+
38+
def parse_string(s):
39+
assert s[0] == '"'
40+
assert s[-1] == '"'
41+
return s[1:-1]
42+
43+
def process_commands(fname):
44+
"""Find and parse dispatch table in implementation file `fname`."""
45+
cmds = []
46+
in_rpcs = False
47+
with open(fname, "r") as f:
48+
for line in f:
49+
line = line.rstrip()
50+
if not in_rpcs:
51+
if re.match("static const CRPCCommand .*\[\] =", line):
52+
in_rpcs = True
53+
else:
54+
if line.startswith('};'):
55+
in_rpcs = False
56+
elif '{' in line and '"' in line:
57+
m = re.search('{ *("[^"]*"), *("[^"]*"), *&([^,]*), *{([^}]*)} *},', line)
58+
assert m, 'No match to table expression: %s' % line
59+
name = parse_string(m.group(2))
60+
args_str = m.group(4).strip()
61+
if args_str:
62+
args = [RPCArgument(parse_string(x.strip()).split('|'), idx) for idx, x in enumerate(args_str.split(','))]
63+
else:
64+
args = []
65+
cmds.append(RPCCommand(name, args))
66+
assert not in_rpcs, "Something went wrong with parsing the C++ file: update the regexps"
67+
return cmds
68+
69+
def process_mapping(fname):
70+
"""Find and parse conversion table in implementation file `fname`."""
71+
cmds = []
72+
in_rpcs = False
73+
with open(fname, "r") as f:
74+
for line in f:
75+
line = line.rstrip()
76+
if not in_rpcs:
77+
if line == 'static const CRPCConvertParam vRPCConvertParams[] =':
78+
in_rpcs = True
79+
else:
80+
if line.startswith('};'):
81+
in_rpcs = False
82+
elif '{' in line and '"' in line:
83+
m = re.search('{ *("[^"]*"), *([0-9]+) *, *("[^"]*") *},', line)
84+
assert m, 'No match to table expression: %s' % line
85+
name = parse_string(m.group(1))
86+
idx = int(m.group(2))
87+
argname = parse_string(m.group(3))
88+
cmds.append((name, idx, argname))
89+
assert not in_rpcs
90+
return cmds
91+
92+
def main():
93+
root = sys.argv[1]
94+
95+
# Get all commands from dispatch tables
96+
cmds = []
97+
for fname in SOURCES:
98+
cmds += process_commands(os.path.join(root, fname))
99+
100+
cmds_by_name = {}
101+
for cmd in cmds:
102+
cmds_by_name[cmd.name] = cmd
103+
104+
# Get current convert mapping for client
105+
client = SOURCE_CLIENT
106+
mapping = set(process_mapping(os.path.join(root, client)))
107+
108+
print('* Checking consistency between dispatch tables and vRPCConvertParams')
109+
110+
# Check mapping consistency
111+
errors = 0
112+
for (cmdname, argidx, argname) in mapping:
113+
try:
114+
rargnames = cmds_by_name[cmdname].args[argidx].names
115+
except IndexError:
116+
print('ERROR: %s argument %i (named %s in vRPCConvertParams) is not defined in dispatch table' % (cmdname, argidx, argname))
117+
errors += 1
118+
continue
119+
if argname not in rargnames:
120+
print('ERROR: %s argument %i is named %s in vRPCConvertParams but %s in dispatch table' % (cmdname, argidx, argname, rargnames), file=sys.stderr)
121+
errors += 1
122+
123+
# Check for conflicts in vRPCConvertParams conversion
124+
# All aliases for an argument must either be present in the
125+
# conversion table, or not. Anything in between means an oversight
126+
# and some aliases won't work.
127+
for cmd in cmds:
128+
for arg in cmd.args:
129+
convert = [((cmd.name, arg.idx, argname) in mapping) for argname in arg.names]
130+
if any(convert) != all(convert):
131+
print('ERROR: %s argument %s has conflicts in vRPCConvertParams conversion specifier %s' % (cmd.name, arg.names, convert))
132+
errors += 1
133+
arg.convert = all(convert)
134+
135+
# Check for conversion difference by argument name.
136+
# It is preferable for API consistency that arguments with the same name
137+
# have the same conversion, so bin by argument name.
138+
all_methods_by_argname = defaultdict(list)
139+
converts_by_argname = defaultdict(list)
140+
for cmd in cmds:
141+
for arg in cmd.args:
142+
for argname in arg.names:
143+
all_methods_by_argname[argname].append(cmd.name)
144+
converts_by_argname[argname].append(arg.convert)
145+
146+
for argname, convert in converts_by_argname.items():
147+
if all(convert) != any(convert):
148+
if argname in IGNORE_DUMMY_ARGS:
149+
# these are testing or dummy, don't warn for them
150+
continue
151+
print('WARNING: conversion mismatch for argument named %s (%s)' %
152+
(argname, list(zip(all_methods_by_argname[argname], converts_by_argname[argname]))))
153+
154+
sys.exit(errors > 0)
155+
156+
157+
if __name__ == '__main__':
158+
main()

0 commit comments

Comments
 (0)