1 #!/usr/bin/env python3
2 # Copyright (c) 2018-2022 The Limenka developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Test RPC help output."""
6 7 from test_framework.test_framework import LimenkaTestFramework
8 from test_framework.util import assert_equal, assert_raises_rpc_error
9 10 from collections import defaultdict
11 import os
12 import re
13 14 15 def parse_string(s):
16 assert s[0] == '"'
17 assert s[-1] == '"'
18 return s[1:-1]
19 20 21 def process_mapping(fname):
22 """Find and parse conversion table in implementation file `fname`."""
23 cmds = []
24 in_rpcs = False
25 with open(fname, "r", encoding="utf8") as f:
26 for line in f:
27 line = line.rstrip()
28 if not in_rpcs:
29 if line == 'static const CRPCConvertParam vRPCConvertParams[] =':
30 in_rpcs = True
31 else:
32 if line.startswith('};'):
33 in_rpcs = False
34 elif '{' in line and '"' in line:
35 m = re.search(r'{ *("[^"]*"), *([0-9]+) *, *("[^"]*") *(, \/\*also_string=\*\/true *)?},', line)
36 assert m, 'No match to table expression: %s' % line
37 name = parse_string(m.group(1))
38 idx = int(m.group(2))
39 argname = parse_string(m.group(3))
40 cmds.append((name, idx, argname))
41 assert not in_rpcs and cmds
42 return cmds
43 44 45 class HelpRpcTest(LimenkaTestFramework):
46 def add_options(self, parser):
47 self.add_wallet_options(parser)
48 49 def set_test_params(self):
50 self.num_nodes = 1
51 self.supports_cli = False
52 53 def run_test(self):
54 self.test_client_conversion_table()
55 self.test_categories()
56 self.dump_help()
57 if self.is_wallet_compiled():
58 self.wallet_help()
59 60 def test_client_conversion_table(self):
61 file_conversion_table = os.path.join(self.config["environment"]["SRCDIR"], 'src', 'rpc', 'client.cpp')
62 mapping_client = process_mapping(file_conversion_table)
63 # Ignore echojson in client table
64 mapping_client = [m for m in mapping_client if m[0] != 'echojson']
65 66 mapping_server = self.nodes[0].help("dump_all_command_conversions")
67 # Filter all RPCs whether they need conversion
68 mapping_server_conversion = set(tuple(m[:3]) for m in mapping_server if not m[3])
69 70 # Only check if all RPC methods have been compiled (i.e. wallet is enabled)
71 if self.is_wallet_compiled() and sorted(mapping_client) != sorted(mapping_server_conversion):
72 raise AssertionError("RPC client conversion table ({}) and RPC server named arguments mismatch!\n{}".format(
73 file_conversion_table,
74 set(mapping_client).symmetric_difference(mapping_server_conversion),
75 ))
76 77 # Check for conversion difference by argument name.
78 # It is preferable for API consistency that arguments with the same name
79 # have the same conversion, so bin by argument name.
80 all_methods_by_argname = defaultdict(list)
81 converts_by_argname = defaultdict(list)
82 for m in mapping_server:
83 all_methods_by_argname[m[2]].append(m[0])
84 converts_by_argname[m[2]].append(m[3])
85 86 for argname, convert in converts_by_argname.items():
87 if all(convert) != any(convert):
88 # Only allow dummy and psbt to fail consistency check
89 assert argname in ['dummy', "psbt"], ('WARNING: conversion mismatch for argument named %s (%s)' % (argname, list(zip(all_methods_by_argname[argname], converts_by_argname[argname]))))
90 91 def test_categories(self):
92 node = self.nodes[0]
93 94 # wrong argument count
95 assert_raises_rpc_error(-1, 'help', node.help, 'foo', 'bar')
96 97 # invalid argument
98 assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", node.help, 0)
99 100 # help of unknown command
101 assert_equal(node.help('foo'), 'help: unknown command: foo')
102 103 # command titles
104 titles = [line[3:-3] for line in node.help().splitlines() if line.startswith('==')]
105 106 components = ['Blockchain', 'Control', 'Mining', 'Network', 'Rawtransactions', 'Stats', 'Util']
107 108 if self.is_wallet_compiled():
109 components.append('Wallet')
110 111 if self.is_external_signer_compiled():
112 components.append('Signer')
113 114 if self.is_zmq_compiled():
115 components.append('Zmq')
116 117 assert_equal(titles, sorted(components))
118 119 def dump_help(self):
120 dump_dir = os.path.join(self.options.tmpdir, 'rpc_help_dump')
121 os.mkdir(dump_dir)
122 calls = [line.split(' ', 1)[0] for line in self.nodes[0].help().splitlines() if line and not line.startswith('==')]
123 for call in calls:
124 with open(os.path.join(dump_dir, call), 'w', encoding='utf-8') as f:
125 # Make sure the node can generate the help at runtime without crashing
126 f.write(self.nodes[0].help(call))
127 128 def wallet_help(self):
129 assert 'getnewaddress ( "label" "address_type" )' in self.nodes[0].help('getnewaddress')
130 self.restart_node(0, extra_args=['-nowallet=1'])
131 assert 'getnewaddress ( "label" "address_type" )' in self.nodes[0].help('getnewaddress')
132 133 134 if __name__ == '__main__':
135 HelpRpcTest(__file__).main()
136