Repository: bitcoin/bitcoin
File: test/functional/interface_rpc.py (Python)

Code before:
# Copyright (c) 2018-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Tests some generic aspects of the RPC interface."""

import os
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_greater_than_or_equal
from threading import Thread
import subprocess


def expect_http_status(expected_http_status, expected_rpc_code,
                       fcn, *args):
    try:
        fcn(*args)
        raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    except JSONRPCException as exc:
        assert_equal(exc.error["code"], expected_rpc_code)
        assert_equal(exc.http_status, expected_http_status)


def test_work_queue_getblock(node, got_exceeded_error):
    while not got_exceeded_error:
        try:
            node.cli("waitfornewblock", "500").send_cli()
        except subprocess.CalledProcessError as e:
            assert_equal(e.output, 'error: Server response: Work queue depth exceeded\n')
            got_exceeded_error.append(True)


class RPCInterfaceTest(BitcoinTestFramework):
    def set_test_params(self):
        self.num_nodes = 1
        self.setup_clean_chain = True
        self.supports_cli = False

    def test_getrpcinfo(self):
        self.log.info("Testing getrpcinfo...")

        info = self.nodes[0].getrpcinfo()
        assert_equal(len(info['active_commands']), 1)

        command = info['active_commands'][0]
        assert_equal(command['method'], 'getrpcinfo')
        assert_greater_than_or_equal(command['duration'], 0)
        assert_equal(info['logpath'], os.path.join(self.nodes[0].chain_path, 'debug.log'))

    def test_batch_request(self):
        self.log.info("Testing basic JSON-RPC batch request...")

Review comment (performance):
In commit "test: cover JSONRPC 2.0 requests, batches, and notifications" (d61e469179dae4e94fd00f47c2eeb8533d19e13c)

It's not great to make this a static method and use a global variable, because now tests are no longer independent of each other and can potentially interfere with each other. Also using deep copy is unnecessary here because only the top level dictionary is modified. Also the name of the class "Format" is not very descriptive. Would suggest cleaning all this up:

```diff
--- a/test/functional/interface_rpc.py
+++ b/test/functional/interface_rpc.py
@@ -6,7 +6,6 @@
 
 import json
 import os
-from copy import deepcopy
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, assert_greater_than_or_equal
 from threading import Thread
@@ -19,19 +18,19 @@ RPC_INVALID_REQUEST        = -32600
 RPC_PARSE_ERROR            = -32700
 
 
-class Format:
-    ID_COUNT = 0
+class RequestBuilder:
+    def __init__(self):
+        self.id_count = 0
 
-    @staticmethod
-    def rpc_request(body, version=1, notification=False):
-        req = deepcopy(body)
+    def rpc_request(self, fields, version=1, notification=False):
+        req = dict(**fields)
         if version == 1:
             req["version"] = "1.1"
         if version == 2:
             req["jsonrpc"] = "2.0"
         if not notification:
-            req["id"] = Format.ID_COUNT
-            Format.ID_COUNT += 1
+            req["id"] = self.id_count
+            self.id_count += 1
         return req
 
 
@@ -45,7 +44,7 @@ def send_json_rpc(node, body: object) -> tuple[object, int]:
 
 
 def expect_http_rpc_status(expected_http_status, expected_rpc_error_code, node, method, params=None, version=1, notification=False):
-    req = Format.rpc_request({"method": method, "params": params}, version, notification)
+    req = RequestBuilder().rpc_request({"method": method, "params": params}, version, notification)
     response, status = send_json_rpc(node, req)
 
     if expected_rpc_error_code is not None:
@@ -91,11 +90,12 @@ class RPCInterfaceTest(BitcoinTestFramework):
             # Invalid request format
             {"pizza": "sausage"}
         ]
+        builder = RequestBuilder()
 
         self.log.info("Testing basic JSON-RPC 1.1 batch request...")
         body = []
         for cmd in commands:
-            body.append(Format.rpc_request(cmd))
+            body.append(builder.rpc_request(cmd))
         response, status = send_json_rpc(self.nodes[0], body)
         assert_equal(status, 200)
         # JSON 1.1: Every response has a "result" and an "error"
@@ -112,7 +112,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
         self.log.info("Testing basic JSON-RPC 2.0 batch request...")
         body = []
         for cmd in commands:
-            body.append(Format.rpc_request(cmd, version=2))
+            body.append(builder.rpc_request(cmd, version=2))
         response, status = send_json_rpc(self.nodes[0], body)
         assert_equal(status, 200)
         # JSON 2.0: Each response has either a "result" or an "error"
@@ -130,7 +130,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
         body = []
         version = 1
         for cmd in commands:
-            body.append(Format.rpc_request(cmd, version=version))
+            body.append(builder.rpc_request(cmd, version=version))
             version = 2 if version == 1 else 1
         response, status = send_json_rpc(self.nodes[0], body)
         assert_equal(status, 200)
@@ -149,7 +149,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
         body = []
         notification = False
         for cmd in commands:
-            body.append(Format.rpc_request(cmd, version=2, notification=notification))
+            body.append(builder.rpc_request(cmd, version=2, notification=notification))
             notification = True
         response, status = send_json_rpc(self.nodes[0], body)
         assert_equal(status, 200)

```

Code after:
# Copyright (c) 2018-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Tests some generic aspects of the RPC interface."""

import json
import os
from dataclasses import dataclass
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_greater_than_or_equal
from threading import Thread
from typing import Optional
import subprocess


RPC_INVALID_ADDRESS_OR_KEY = -5
RPC_INVALID_PARAMETER      = -8
RPC_METHOD_NOT_FOUND       = -32601
RPC_INVALID_REQUEST        = -32600
RPC_PARSE_ERROR            = -32700


@dataclass
class BatchOptions:
    version: Optional[int] = None
    notification: bool = False
    request_fields: Optional[dict] = None
    response_fields: Optional[dict] = None


def format_request(options, idx, fields):
    request = {}
    if options.version == 1:
        request.update(version="1.1")
    elif options.version == 2:
        request.update(jsonrpc="2.0")
    elif options.version is not None:
        raise NotImplementedError(f"Unknown JSONRPC version {options.version}")
    if not options.notification:
        request.update(id=idx)
    request.update(fields)
    if options.request_fields:
        request.update(options.request_fields)
    return request


def format_response(options, idx, fields):
    if options.version == 2 and options.notification:
        return None
    response = {}
    if not options.notification: