rpc_help.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2018-2022 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 """Test RPC help output.""" 6 7 from test_framework.test_framework import BitcoinTestFramework 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]+) *, *("[^"]*") *},', 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(BitcoinTestFramework): 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 = [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', '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().main()