rpc_help.py raw

   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