1#!/usr/bin/env python3
2# Copyright (c) 2015-2018 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"""Utilities for doing coverage analysis on the RPC interface.
6
7Provides a way to track which RPC commands are exercised during
8testing.
9"""
10
11import os
12
13
14REFERENCE_FILENAME = 'rpc_interface.txt'
15
16
17class AuthServiceProxyWrapper():
18    """
19    An object that wraps AuthServiceProxy to record specific RPC calls.
20
21    """
22    def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
23        """
24        Kwargs:
25            auth_service_proxy_instance (AuthServiceProxy): the instance
26                being wrapped.
27            coverage_logfile (str): if specified, write each service_name
28                out to a file when called.
29
30        """
31        self.auth_service_proxy_instance = auth_service_proxy_instance
32        self.coverage_logfile = coverage_logfile
33
34    def __getattr__(self, name):
35        return_val = getattr(self.auth_service_proxy_instance, name)
36        if not isinstance(return_val, type(self.auth_service_proxy_instance)):
37            # If proxy getattr returned an unwrapped value, do the same here.
38            return return_val
39        return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
40
41    def __call__(self, *args, **kwargs):
42        """
43        Delegates to AuthServiceProxy, then writes the particular RPC method
44        called to a file.
45
46        """
47        return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
48        self._log_call()
49        return return_val
50
51    def _log_call(self):
52        rpc_method = self.auth_service_proxy_instance._service_name
53
54        if self.coverage_logfile:
55            with open(self.coverage_logfile, 'a+', encoding='utf8') as f:
56                f.write("%s\n" % rpc_method)
57
58    def __truediv__(self, relative_uri):
59        return AuthServiceProxyWrapper(self.auth_service_proxy_instance / relative_uri,
60                                       self.coverage_logfile)
61
62    def get_request(self, *args, **kwargs):
63        self._log_call()
64        return self.auth_service_proxy_instance.get_request(*args, **kwargs)
65
66def get_filename(dirname, n_node):
67    """
68    Get a filename unique to the test process ID and node.
69
70    This file will contain a list of RPC commands covered.
71    """
72    pid = str(os.getpid())
73    return os.path.join(
74        dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
75
76
77def write_all_rpc_commands(dirname, node):
78    """
79    Write out a list of all RPC functions available in `bitcoin-cli` for
80    coverage comparison. This will only happen once per coverage
81    directory.
82
83    Args:
84        dirname (str): temporary test dir
85        node (AuthServiceProxy): client
86
87    Returns:
88        bool. if the RPC interface file was written.
89
90    """
91    filename = os.path.join(dirname, REFERENCE_FILENAME)
92
93    if os.path.isfile(filename):
94        return False
95
96    help_output = node.help().split('\n')
97    commands = set()
98
99    for line in help_output:
100        line = line.strip()
101
102        # Ignore blanks and headers
103        if line and not line.startswith('='):
104            commands.add("%s\n" % line.split()[0])
105
106    with open(filename, 'w', encoding='utf8') as f:
107        f.writelines(list(commands))
108
109    return True
110